﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;
using System.Diagnostics.Contracts;
using System.Threading;
using System.Xml;

namespace UPNP.IO
{
    partial class DeviceService
    {

        private delegate object CachedGetVariableDelegate();

        #region enumeration

        public IEnumerable<KeyValuePair<string, object>> GetAllVariableValues()
        {
            foreach (var v in _serviceDom.StateVariables)
            {
                object val;

                try
                {
                    val = LoadVariable(v.VariableName);
                }
                catch (Exception x)
                {
                    val = x;
                }

                yield return new KeyValuePair<string, object>(v.VariableName, val);
            }
        }

        #endregion

        [MethodImpl(MethodImplOptions.Synchronized)]
        protected object LoadVariable(string name)
        {
            Contract.Requires(!string.IsNullOrEmpty(name));

            // find variable
            var variable = (
                from v in _serviceDom.StateVariables
                where v.VariableName == name
                select v
                ).FirstOrDefault();

            if (variable == null)
            {
                throw new NotImplementedException(string.Format(
                    "Variable does not exist in device schema of service '{1}': {0}",
                    name,
                    this.ServiceTypeIdentifier
                    ));
            }

            // log
            if (_logIO.IsDebugEnabled)
            {
                _logIO.DebugFormat(
                    "Querying variable on service '{1}': {0}",
                    name,
                    this.ServiceTypeIdentifier
                    );
            }

            // cached?
            var cacheName = ("V_" + name);
            var getVar = _expressionCache[cacheName] as CachedGetVariableDelegate;
            var cached = true;

            // create new
            if (getVar == null)
            {
                cached = false;

                // create and compile expression
                getVar = () => LoadVariableInternal(variable);
            }

            // load then
            object r;

            try
            {
                r = getVar();
            }
            catch (NotImplementedException x)
            {
                // make sure we cache the error
                if (!cached)
                {
                    getVar = () => RethrowException(x);
                }

                // forward error
                throw;
            }

            // add to cache
            if (!cached)
                _expressionCache.Add(cacheName, getVar);

            // finalize
            return r;
        }

        private object RethrowException(Exception x)
        {
            if (this.ConnectionLost)
                throw new DeviceConnectionLostException(this.ParentDevice);

            throw x;
        }

        private object LoadVariableInternal(DeviceDom.StateVariable var)
        {
            Contract.Requires(var != null);

            // continue
            try
            {
                // connection lost?
                if (this.ConnectionLost)
                    throw new DeviceConnectionLostException(_parentDevice);

                // query variable
                var query = new QueryVariableQuery(var);

                var val = query.DoQuery();

                // done
                if (_logIO.IsDebugEnabled)
                {
                    _logIO.DebugFormat(
                        "Queried variable of service '{2}': {0} = {1}", 
                        var.VariableName, 
                        val,
                        this.ServiceTypeIdentifier
                        );
                }

                return val;
            }
            catch (NotImplementedException)
            {
                // just passing through
                throw;
            }
            catch (DeviceConnectionLostException)
            {
                if (!_parentDevice.RootDevice.ConnectionLost)
                    _parentDevice.RootDevice.SetConnectionLost();

                throw;
            }
            catch (Exception e)
            {
                _logIO.Error(string.Format(
                    "Failed to query variable of service '{1}': {0}",
                    var.VariableName,
                    this.ServiceTypeIdentifier
                    ), e);

                throw;
            }
        }

        #region soap request

        internal class QueryVariableQuery : DeviceServiceQuery
        {
            private readonly DeviceDom.StateVariable _dom;

            protected override void PrepareRequest(HttpWebRequest req)
            {
                req.Method = "POST";
                req.ContentType = "text/xml; charset=\"" + SoapEncoding.WebName + "\"";

                req.Headers.Add("SOAPACTION", "\"" + _dom.OwnerDom.ControlNamespace + "#QueryStateVariable\"");
            }

            protected override void WriteRequest(XmlTextWriter xw)
            {
                xw.WriteStartDocument();
                {
                    _dom.ToSoapRequest(xw);
                }
                xw.WriteEndDocument();
            }

            protected override object HandleResponse(HttpWebResponse response, XmlDocument body)
            {
                var valElement = body.SelectSingleNode(
                    "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='QueryStateVariableResponse']/*[local-name()='return']"
                    );

                if (valElement == null)
                {
                    throw new DeviceCommunicationException("variable return value element not found");
                }

                // convert the value
                return _dom.ValueConverter.FromDevice(valElement.InnerText);
            }

            public QueryVariableQuery(DeviceDom.StateVariable v)
                : base(v.ParentService, _logIO)
            {
                Contract.Requires(v != null);

                _dom = v;
            }
        }

        internal sealed class QueryVariableQueryMan : QueryVariableQuery
        {
            protected override void PrepareRequest(HttpWebRequest req)
            {
                req.Method = "M-POST";
                req.ContentType = "text/xml; charset=\"" + SoapEncoding.WebName + "\"";

                req.Headers.Add("MAN", "\"http://schemas.xmlsoap.org/soap/envelope/\"; ns=01");
                req.Headers.Add("01-SOAPACTION", "\"urn:schemas-upnp-org:control-1-0#QueryStateVariable\"");
            }

            protected override object HandleResponse(HttpWebResponse response, XmlDocument body)
            {
                // ensure MAN is supported
                if (!response.Headers.AllKeys.Contains("EXT"))
                    throw new DeviceCommunicationException("MAN/EXT not understood");

                // continue
                return base.HandleResponse(response, body);
            }

            public QueryVariableQueryMan(DeviceDom.StateVariable v)
                : base(v)
            {
                Contract.Requires(v != null);
            }
        }

        #endregion

    }

}
